/*
 * Copyright (c) 2022 DuckDuckGo
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.duckduckgo.autofill.impl

import android.webkit.JavascriptInterface
import android.webkit.WebView
import com.duckduckgo.app.di.AppCoroutineScope
import com.duckduckgo.autofill.api.AutofillCapabilityChecker
import com.duckduckgo.autofill.api.Callback
import com.duckduckgo.autofill.api.CredentialUpdateExistingCredentialsDialog.CredentialUpdateType
import com.duckduckgo.autofill.api.EmailProtectionInContextSignupFlowListener
import com.duckduckgo.autofill.api.EmailProtectionUserPromptListener
import com.duckduckgo.autofill.api.ExistingCredentialMatchDetector
import com.duckduckgo.autofill.api.domain.app.LoginCredentials
import com.duckduckgo.autofill.api.domain.app.LoginTriggerType
import com.duckduckgo.autofill.api.email.EmailManager
import com.duckduckgo.autofill.api.passwordgeneration.AutomaticSavedLoginsMonitor
import com.duckduckgo.autofill.impl.deduper.AutofillLoginDeduplicator
import com.duckduckgo.autofill.impl.domain.javascript.JavascriptCredentials
import com.duckduckgo.autofill.impl.email.incontext.availability.EmailProtectionInContextRecentInstallChecker
import com.duckduckgo.autofill.impl.email.incontext.store.EmailProtectionInContextDataStore
import com.duckduckgo.autofill.impl.importing.InBrowserImportPromo
import com.duckduckgo.autofill.impl.jsbridge.AutofillMessagePoster
import com.duckduckgo.autofill.impl.jsbridge.request.AutofillDataRequest
import com.duckduckgo.autofill.impl.jsbridge.request.AutofillRequestParser
import com.duckduckgo.autofill.impl.jsbridge.request.AutofillStoreFormDataCredentialsRequest
import com.duckduckgo.autofill.impl.jsbridge.request.AutofillStoreFormDataRequest
import com.duckduckgo.autofill.impl.jsbridge.request.FormSubmissionTriggerType.FORM_SUBMISSION
import com.duckduckgo.autofill.impl.jsbridge.request.FormSubmissionTriggerType.PARTIAL_SAVE
import com.duckduckgo.autofill.impl.jsbridge.request.FormSubmissionTriggerType.UNKNOWN
import com.duckduckgo.autofill.impl.jsbridge.request.SupportedAutofillInputMainType.CREDENTIALS
import com.duckduckgo.autofill.impl.jsbridge.request.SupportedAutofillInputSubType.PASSWORD
import com.duckduckgo.autofill.impl.jsbridge.request.SupportedAutofillInputSubType.USERNAME
import com.duckduckgo.autofill.impl.jsbridge.request.SupportedAutofillTriggerType
import com.duckduckgo.autofill.impl.jsbridge.request.SupportedAutofillTriggerType.AUTOPROMPT
import com.duckduckgo.autofill.impl.jsbridge.request.SupportedAutofillTriggerType.CREDENTIALS_IMPORT
import com.duckduckgo.autofill.impl.jsbridge.request.SupportedAutofillTriggerType.USER_INITIATED
import com.duckduckgo.autofill.impl.jsbridge.response.AutofillResponseWriter
import com.duckduckgo.autofill.impl.partialsave.PartialCredentialSaveStore
import com.duckduckgo.autofill.impl.partialsave.UsernameBackFiller
import com.duckduckgo.autofill.impl.partialsave.UsernameBackFiller.BackFillResult.BackFillNotSupported
import com.duckduckgo.autofill.impl.partialsave.UsernameBackFiller.BackFillResult.BackFillSupported
import com.duckduckgo.autofill.impl.sharedcreds.ShareableCredentials
import com.duckduckgo.autofill.impl.store.InternalAutofillStore
import com.duckduckgo.autofill.impl.store.NeverSavedSiteRepository
import com.duckduckgo.autofill.impl.systemautofill.SystemAutofillServiceSuppressor
import com.duckduckgo.autofill.impl.ui.credential.passwordgeneration.Actions
import com.duckduckgo.autofill.impl.ui.credential.passwordgeneration.Actions.DeleteAutoLogin
import com.duckduckgo.autofill.impl.ui.credential.passwordgeneration.Actions.DiscardAutoLoginId
import com.duckduckgo.autofill.impl.ui.credential.passwordgeneration.Actions.PromptToSave
import com.duckduckgo.autofill.impl.ui.credential.passwordgeneration.Actions.UpdateMatchingUsernames
import com.duckduckgo.autofill.impl.ui.credential.passwordgeneration.Actions.UpdateSavedAutoLogin
import com.duckduckgo.autofill.impl.ui.credential.passwordgeneration.AutogeneratedPasswordEventResolver
import com.duckduckgo.common.utils.ConflatedJob
import com.duckduckgo.common.utils.DefaultDispatcherProvider
import com.duckduckgo.common.utils.DispatcherProvider
import com.duckduckgo.di.scopes.AppScope
import com.squareup.anvil.annotations.ContributesBinding
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import logcat.LogPriority.ERROR
import logcat.LogPriority.INFO
import logcat.LogPriority.VERBOSE
import logcat.LogPriority.WARN
import logcat.asLog
import logcat.logcat
import javax.inject.Inject

interface AutofillJavascriptInterface {

    @JavascriptInterface
    fun getAutofillData(requestString: String)

    @JavascriptInterface
    fun getIncontextSignupDismissedAt(data: String)

    fun injectCredentials(credentials: LoginCredentials)
    fun injectNoCredentials()

    fun cancelRetrievingStoredLogins()

    fun acceptGeneratedPassword()
    fun rejectGeneratedPassword()

    fun inContextEmailProtectionFlowFinished()

    var callback: Callback?
    var emailProtectionInContextCallback: EmailProtectionUserPromptListener?
    var emailProtectionInContextSignupFlowCallback: EmailProtectionInContextSignupFlowListener?
    var webView: WebView?
    var autoSavedLoginsMonitor: AutomaticSavedLoginsMonitor?
    var tabId: String?

    companion object {
        const val INTERFACE_NAME = "BrowserAutofill"
    }

    @JavascriptInterface
    fun closeEmailProtectionTab(data: String)
}

@ContributesBinding(AppScope::class)
class AutofillStoredBackJavascriptInterface @Inject constructor(
    private val requestParser: AutofillRequestParser,
    private val autofillStore: InternalAutofillStore,
    private val shareableCredentials: ShareableCredentials,
    private val autofillMessagePoster: AutofillMessagePoster,
    private val autofillResponseWriter: AutofillResponseWriter,
    @AppCoroutineScope private val coroutineScope: CoroutineScope,
    private val dispatcherProvider: DispatcherProvider = DefaultDispatcherProvider(),
    private val currentUrlProvider: UrlProvider = WebViewUrlProvider(dispatcherProvider),
    private val autofillCapabilityChecker: AutofillCapabilityChecker,
    private val passwordEventResolver: AutogeneratedPasswordEventResolver,
    private val emailManager: EmailManager,
    private val inContextDataStore: EmailProtectionInContextDataStore,
    private val recentInstallChecker: EmailProtectionInContextRecentInstallChecker,
    private val loginDeduplicator: AutofillLoginDeduplicator,
    private val systemAutofillServiceSuppressor: SystemAutofillServiceSuppressor,
    private val neverSavedSiteRepository: NeverSavedSiteRepository,
    private val partialCredentialSaveStore: PartialCredentialSaveStore,
    private val usernameBackFiller: UsernameBackFiller,
    private val existingCredentialMatchDetector: ExistingCredentialMatchDetector,
    private val inBrowserImportPromo: InBrowserImportPromo,
) : AutofillJavascriptInterface {

    override var callback: Callback? = null
    override var emailProtectionInContextCallback: EmailProtectionUserPromptListener? = null
    override var emailProtectionInContextSignupFlowCallback: EmailProtectionInContextSignupFlowListener? = null
    override var webView: WebView? = null
    override var autoSavedLoginsMonitor: AutomaticSavedLoginsMonitor? = null
    override var tabId: String? = null

    // coroutine jobs tracked for supporting cancellation
    private val getAutofillDataJob = ConflatedJob()
    private val storeFormDataJob = ConflatedJob()
    private val injectCredentialsJob = ConflatedJob()
    private val emailProtectionInContextSignupJob = ConflatedJob()

    @JavascriptInterface
    override fun getAutofillData(requestString: String) {
        logcat(VERBOSE) { "BrowserAutofill: getAutofillData called:\n$requestString" }
        getAutofillDataJob += coroutineScope.launch(dispatcherProvider.io()) {
            val url = currentUrlProvider.currentUrl(webView)
            if (url == null) {
                logcat(WARN) { "Can't autofill as can't retrieve current URL" }
                return@launch
            }

            if (!autofillCapabilityChecker.canInjectCredentialsToWebView(url)) {
                logcat(VERBOSE) { "BrowserAutofill: getAutofillData called but feature is disabled" }
                return@launch
            }

            val parseResult = requestParser.parseAutofillDataRequest(requestString)
            val request = parseResult.getOrElse {
                logcat(WARN) { "Unable to parse getAutofillData request: ${it.asLog()}" }
                return@launch
            }

            val triggerType = convertTriggerType(request.trigger)

            if (request.mainType != CREDENTIALS) {
                handleUnknownRequestMainType(request, url)
                return@launch
            }

            if (request.isGeneratedPasswordAvailable()) {
                handleRequestForPasswordGeneration(url, request)
            } else if (request.isAutofillCredentialsRequest()) {
                handleRequestForAutofillingCredentials(url, request, triggerType)
            } else {
                logcat(WARN) { "Unable to process request; don't know how to handle request $requestString" }
            }
        }
    }

    private fun handlePromoteImport(url: String) {
        coroutineScope.launch(dispatcherProvider.io()) {
            callback?.promptUserToImportPassword(url)
        }
    }

    @JavascriptInterface
    override fun getIncontextSignupDismissedAt(data: String) {
        emailProtectionInContextSignupJob += coroutineScope.launch(dispatcherProvider.io()) {
            val permanentDismissalTime = inContextDataStore.timestampUserChoseNeverAskAgain()
            val installedRecently = recentInstallChecker.isRecentInstall()
            val jsonResponse = autofillResponseWriter.generateResponseForEmailProtectionInContextSignup(installedRecently, permanentDismissalTime)
            autofillMessagePoster.postMessage(webView, jsonResponse)
        }
    }

    @JavascriptInterface
    override fun closeEmailProtectionTab(data: String) {
        emailProtectionInContextSignupFlowCallback?.closeInContextSignup()
    }

    @Suppress("UNUSED_PARAMETER")
    @JavascriptInterface
    fun showInContextEmailProtectionSignupPrompt(data: String) {
        coroutineScope.launch(dispatcherProvider.io()) {
            currentUrlProvider.currentUrl(webView)?.let {
                val isSignedIn = emailManager.isSignedIn()

                withContext(dispatcherProvider.main()) {
                    if (isSignedIn) {
                        emailProtectionInContextCallback?.showNativeChooseEmailAddressPrompt()
                    } else {
                        emailProtectionInContextCallback?.showNativeInContextEmailProtectionSignupPrompt()
                    }
                }
            }
        }
    }

    private suspend fun handleRequestForPasswordGeneration(
        url: String,
        request: AutofillDataRequest,
    ) {
        callback?.onGeneratedPasswordAvailableToUse(url, request.generatedPassword?.username, request.generatedPassword?.value!!)
    }

    private suspend fun handleRequestForAutofillingCredentials(
        url: String,
        request: AutofillDataRequest,
        triggerType: LoginTriggerType,
    ) {
        val matches = mutableListOf<LoginCredentials>()
        val directMatches = autofillStore.getCredentials(url)
        val shareableMatches = shareableCredentials.shareableCredentials(url)
        logcat(VERBOSE) { "Direct matches: ${directMatches.size}, shareable matches: ${shareableMatches.size} for $url" }
        matches.addAll(directMatches)
        matches.addAll(shareableMatches)

        val credentials = filterRequestedSubtypes(request, matches)

        val dedupedCredentials = loginDeduplicator.deduplicate(url, credentials)
        logcat(VERBOSE) { "Original autofill credentials list size: ${credentials.size}, after de-duping: ${dedupedCredentials.size}" }

        val finalCredentialList = ensureUsernamesNotNull(dedupedCredentials)

        if (finalCredentialList.isEmpty()) {
            val canShowImport = inBrowserImportPromo.canShowPromo(credentialsAvailableForCurrentPage = false, url = url)
            if (canShowImport) {
                handlePromoteImport(url)
            } else {
                callback?.noCredentialsAvailable(url)
            }
        } else {
            notifyListenerThatCredentialsAvailableToInject(url, finalCredentialList, triggerType, request)
        }
    }

    private suspend fun notifyListenerThatCredentialsAvailableToInject(
        url: String,
        finalCredentialList: List<LoginCredentials>,
        triggerType: LoginTriggerType,
        request: AutofillDataRequest,
    ) {
        when (val currentCallback = callback) {
            is InternalCallback -> {
                currentCallback.onCredentialsAvailableToInjectWithReauth(url, finalCredentialList, triggerType, request.subType)
            }
            else -> {
                currentCallback?.onCredentialsAvailableToInject(url, finalCredentialList, triggerType)
            }
        }
    }

    private fun ensureUsernamesNotNull(credentials: List<LoginCredentials>) =
        credentials.map {
            if (it.username == null) {
                it.copy(username = "")
            } else {
                it
            }
        }

    private fun convertTriggerType(trigger: SupportedAutofillTriggerType): LoginTriggerType {
        return when (trigger) {
            USER_INITIATED -> LoginTriggerType.USER_INITIATED
            AUTOPROMPT -> LoginTriggerType.AUTOPROMPT
            CREDENTIALS_IMPORT -> LoginTriggerType.AUTOPROMPT
        }
    }

    private fun filterRequestedSubtypes(
        request: AutofillDataRequest,
        credentials: List<LoginCredentials>,
    ): List<LoginCredentials> {
        return when (request.subType) {
            USERNAME -> credentials.filterNot { it.username.isNullOrBlank() }
            PASSWORD -> credentials.filterNot { it.password.isNullOrBlank() }
        }
    }

    private fun handleUnknownRequestMainType(
        request: AutofillDataRequest,
        url: String,
    ) {
        logcat(WARN) { "Autofill type ${request.mainType} unsupported" }
        callback?.noCredentialsAvailable(url)
    }

    @JavascriptInterface
    fun storeFormData(data: String) {
        // important to call suppressor as soon as possible
        systemAutofillServiceSuppressor.suppressAutofill(webView)

        logcat(INFO) { "storeFormData called, credentials provided to be persisted" }

        storeFormDataJob += coroutineScope.launch(dispatcherProvider.io()) {
            val currentUrl = currentUrlProvider.currentUrl(webView) ?: return@launch

            if (!autofillCapabilityChecker.canSaveCredentialsFromWebView(currentUrl)) {
                logcat(VERBOSE) { "BrowserAutofill: storeFormData called but feature is disabled" }
                return@launch
            }

            if (neverSavedSiteRepository.isInNeverSaveList(currentUrl)) {
                logcat(VERBOSE) { "BrowserAutofill: storeFormData called but site is in never save list" }
                return@launch
            }

            val parseResult = requestParser.parseStoreFormDataRequest(data)
            val request = parseResult.getOrElse {
                logcat(WARN) { "Unable to parse storeFormData request: ${it.asLog()}" }
                return@launch
            }

            if (!request.isValid()) {
                logcat(WARN) { "Invalid data from storeFormData" }
                return@launch
            }

            val requestCredentials = request.credentials ?: return@launch

            when (request.trigger) {
                FORM_SUBMISSION -> handleRequestForFormSubmission(requestCredentials, currentUrl)
                PARTIAL_SAVE -> handleRequestForPartialSave(requestCredentials, currentUrl)
                UNKNOWN -> logcat(ERROR) { "Unknown trigger type ${request.trigger}" }
            }
        }
    }

    private suspend fun handleRequestForFormSubmission(
        requestCredentials: AutofillStoreFormDataCredentialsRequest,
        currentUrl: String,
    ) {
        val jsCredentials = JavascriptCredentials(requestCredentials.username, requestCredentials.password)

        val backFillUsernameIfRequired = jsCredentials
            .asLoginCredentials(currentUrl)
            .backFillUsernameIfSupported(currentUrl)

        val credentials = backFillUsernameIfRequired.first
        val backFilled = backFillUsernameIfRequired.second

        val autologinId = autoSavedLoginsMonitor?.getAutoSavedLoginId(tabId)
        val autogenerated = requestCredentials.autogenerated
        val autosavedLogin = autologinId?.let { autofillStore.getCredentialsWithId(it) }

        val matchType = existingCredentialMatchDetector.determine(
            currentUrl = currentUrl,
            username = credentials.username,
            password = credentials.password,
        )

        val actions = passwordEventResolver.decideActions(
            autoSavedLogin = autosavedLogin,
            autogenerated = autogenerated,
            matchType = matchType,
            backFilled = backFilled,
            username = credentials.username,
        )
        processStoreFormDataActions(actions, currentUrl, credentials)
    }

    private suspend fun handleRequestForPartialSave(
        requestCredentials: AutofillStoreFormDataCredentialsRequest,
        currentUrl: String,
    ) {
        val username = requestCredentials.username ?: return
        partialCredentialSaveStore.saveUsername(url = currentUrl, username = username)
    }

    private suspend fun processStoreFormDataActions(
        actions: List<Actions>,
        currentUrl: String,
        credentials: LoginCredentials,
    ) {
        logcat { "${actions.size} actions to take: ${actions.joinToString { it.javaClass.simpleName }}" }
        actions.forEach {
            when (it) {
                is DeleteAutoLogin -> {
                    autofillStore.deleteCredentials(it.autologinId)
                }

                is DiscardAutoLoginId -> {
                    autoSavedLoginsMonitor?.clearAutoSavedLoginId(tabId)
                }

                is PromptToSave -> {
                    callback?.onCredentialsAvailableToSave(currentUrl, credentials)
                }

                is UpdateSavedAutoLogin -> {
                    autofillStore.getCredentialsWithId(it.autologinId)?.let { existingCredentials ->
                        if (isUpdateRequired(existingCredentials, credentials)) {
                            logcat(VERBOSE) { "Updating without prompting: autologin not identical to what is already stored. id=${it.autologinId}" }
                            val toSave = existingCredentials.copy(username = credentials.username, password = credentials.password)
                            autofillStore.updateCredentials(toSave)?.let { savedCredentials ->
                                callback?.onCredentialsSaved(savedCredentials)
                            }
                        } else {
                            logcat(VERBOSE) { "Update not required as identical to what is already stored. id=${it.autologinId}" }
                            callback?.onCredentialsSaved(existingCredentials)
                        }
                    }
                }

                is UpdateMatchingUsernames -> {
                    val password = credentials.password ?: return@forEach
                    val updated = updateCredentialsIfPasswordMissing(originalUrl = currentUrl, username = credentials.username, password = password)
                    if (updated) {
                        callback?.onCredentialsSaved(credentials)
                    }
                }
            }
        }
    }

    private fun isUpdateRequired(
        existingCredentials: LoginCredentials,
        credentials: LoginCredentials,
    ): Boolean {
        return existingCredentials.username != credentials.username || existingCredentials.password != credentials.password
    }

    private suspend fun updateCredentialsIfPasswordMissing(
        originalUrl: String,
        username: String?,
        password: String,
    ): Boolean {
        return withContext(dispatcherProvider.io()) {
            var updateMade = false

            autofillStore.getCredentials(originalUrl)
                .filter { it.username != null }
                .filter { it.username == username }
                .filter { it.password.isNullOrEmpty() }
                .also { list ->
                    logcat(VERBOSE) {
                        "Found ${list.size} credentials with missing password for username=$username and url=$originalUrl"
                    }
                }
                .map { it.copy(password = password) }
                .forEach {
                    if (autofillStore.updateCredentials(originalUrl, it, CredentialUpdateType.Password) != null) {
                        updateMade = true
                    }
                }

            updateMade
        }
    }

    private fun AutofillStoreFormDataRequest?.isValid(): Boolean {
        if (this == null || credentials == null) return false
        return !(credentials.username.isNullOrBlank() && credentials.password.isNullOrBlank())
    }

    override fun injectCredentials(credentials: LoginCredentials) {
        logcat(VERBOSE) { "Informing JS layer with credentials selected" }
        injectCredentialsJob += coroutineScope.launch(dispatcherProvider.io()) {
            val jsCredentials = credentials.asJsCredentials()
            val jsonResponse = autofillResponseWriter.generateResponseGetAutofillData(jsCredentials)
            logcat(INFO) { "Injecting credentials: $jsonResponse" }
            autofillMessagePoster.postMessage(webView, jsonResponse)
        }
    }

    override fun injectNoCredentials() {
        logcat(VERBOSE) { "No credentials selected; informing JS layer" }
        injectCredentialsJob += coroutineScope.launch(dispatcherProvider.io()) {
            autofillMessagePoster.postMessage(webView, autofillResponseWriter.generateEmptyResponseGetAutofillData())
        }
    }

    private fun LoginCredentials.asJsCredentials(): JavascriptCredentials {
        return JavascriptCredentials(
            username = username,
            password = password,
        )
    }

    override fun cancelRetrievingStoredLogins() {
        getAutofillDataJob.cancel()
    }

    override fun acceptGeneratedPassword() {
        logcat(VERBOSE) { "Accepting generated password" }
        injectCredentialsJob += coroutineScope.launch(dispatcherProvider.io()) {
            autofillMessagePoster.postMessage(webView, autofillResponseWriter.generateResponseForAcceptingGeneratedPassword())
        }
    }

    override fun rejectGeneratedPassword() {
        logcat(VERBOSE) { "Rejecting generated password" }
        injectCredentialsJob += coroutineScope.launch(dispatcherProvider.io()) {
            autofillMessagePoster.postMessage(webView, autofillResponseWriter.generateResponseForRejectingGeneratedPassword())
        }
    }

    override fun inContextEmailProtectionFlowFinished() {
        emailProtectionInContextSignupJob += coroutineScope.launch(dispatcherProvider.io()) {
            val json = autofillResponseWriter.generateResponseForEmailProtectionEndOfFlow(emailManager.isSignedIn())
            autofillMessagePoster.postMessage(webView, json)
        }
    }

    private fun JavascriptCredentials.asLoginCredentials(
        url: String,
    ): LoginCredentials {
        return LoginCredentials(
            id = null,
            domain = url,
            username = username,
            password = password,
            domainTitle = null,
        )
    }

    private suspend fun LoginCredentials.backFillUsernameIfSupported(currentUrl: String): Pair<LoginCredentials, Boolean> {
        // determine if we can and should use a partial previous submission's username
        val result = usernameBackFiller.isBackFillingUsernameSupported(this.username, currentUrl)
        return when (result) {
            is BackFillSupported -> Pair(this.copy(username = result.username), true)
            is BackFillNotSupported -> Pair(this, false)
        }
    }

    interface UrlProvider {
        suspend fun currentUrl(webView: WebView?): String?
    }

    @ContributesBinding(AppScope::class)
    class WebViewUrlProvider @Inject constructor(val dispatcherProvider: DispatcherProvider) : UrlProvider {
        override suspend fun currentUrl(webView: WebView?): String? {
            return withContext(dispatcherProvider.main()) {
                webView?.url
            }
        }
    }
}
